#region (c) 2014 Nguyễn, M. Hải  - All rights reserved
/*
Simplified BSD License (BSD)
Copyright (c) 2014, Nguyễn, M. Hải ( www.Minh-Hai.com ). All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice, this 
      list of conditions and the following disclaimer.

    * Redistributions in binary form must reproduce the above copyright notice, 
      this list of conditions and the following disclaimer in the documentation 
      and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.
*/
#endregion

#region Record of Change
//             							CHANGE HISTORY
// -------------------------------------------------------------------------------------
// |   DATE    | DEVELOPER  | DESCRIPTION                                              |
// -------------------------------------------------------------------------------------
// | 08-Sep-12 | HaiNM      | First creation.                                          |
// -------------------------------------------------------------------------------------
//
#endregion

#region Record of Audit
//             							REVIEW HISTORY
// -------------------------------------------------------------------------------------
// |   DATE    | REVIEWER   | NOTES                                                    |
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
//
#endregion

#region Using directives
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Controls;
using System.ComponentModel;

using Thn;
#endregion

namespace Thn.Presentation.Controls
{
    /// <summary>
    /// An input box that accepts number values.
    /// </summary>
#if DEBUG || TESTING
    public
#else
    public
#endif
 class NumberBox : TextBox, INotifyPropertyChanged
    {
        #region Fields
        bool mSuppressValueChange;
        #endregion

        #region Properties

        #region Accept Decimals
        private bool mAcceptDecimals = true;
        /// <summary>
        /// Gets or sets whether decimals are allowed.
        /// </summary>
        public bool AcceptDecimals
        {
            get { return mAcceptDecimals; }
            set { mAcceptDecimals = value; }
        }
        #endregion

        #region Accept Negative
        /// <summary>
        /// Checks whether nevative values are acceptable.
        /// </summary>
        public bool AcceptNegative
        {
            get { return mMinValue < 0; }
        }
        #endregion

        #region Min Value
        private decimal mMinValue = decimal.MinValue;
        /// <summary>
        /// Gets or sets the minimum value.
        /// </summary>
        public decimal MinValue
        {
            get { return mMinValue; }
            set { mMinValue = value; }
        }
        #endregion

        #region Max Value
        private decimal mMaxValue = decimal.MaxValue;
        /// <summary>
        /// Gets or sets the maximum value.
        /// </summary>
        public decimal MaxValue
        {
            get { return mMaxValue; }
            set { mMaxValue = value; }
        }
        #endregion

        #region Value
        //private decimal mValue;
        ///// <summary>
        ///// Gets or sets the current value.
        ///// </summary>
        //public decimal Value
        //{
        //    get { return decimal.Parse(this.Text); }
        //    set
        //    {
        //        if (value != null)
        //        {
        //            this.Text = string.Format("{0}", value);
        //        }
        //        else
        //        {
        //            this.Text = "0";
        //        }
        //    }
        //}
        #endregion

        #region Format
        private string mFormat = "#,#0.#####";
        /// <summary>
        /// Gets or sets description.
        /// </summary>
        public string Format
        {
            get { return mFormat; }
            set { mFormat = value; }
        }
        #endregion

        #region Has Focus
        private bool mHasFocus;
        /// <summary>
        /// Checks whether this control is currently receives focus.
        /// </summary>
        public bool HasFocus
        {
            get { return mHasFocus; }
        }
        #endregion

        #region Value
        /// <summary>
        /// Gets or sets value.
        /// </summary>
        [DefaultValue(0)]
        public decimal Value
        {
            get { return (decimal)GetValue(ValueProperty); }
            set
            {
                SetValue(ValueProperty, value);
                //ApplyValueToTextWithFormat();
            }
        }

        /// <summary>
        /// Backing dependency property for Value.
        /// </summary>
        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register(
            "Value"
            , typeof(decimal)
            , typeof(NumberBox)
            , new PropertyMetadata(0m, ValuePropertyChanged)
            );

        /// <summary>
        /// Invoked when value of property Value is changed.
        /// </summary>
        protected virtual void OnValueChanged(DependencyPropertyChangedEventArgs e)
        {
            //if (!mSuppressValueChange)
            //{
            //    if (e.NewValue != null)
            //    {
            //        this.Text = string.Format("{0}", e.NewValue);
            //    }
            //    else
            //    {
            //        this.Text = "0";
            //    }
            //}
            //#region Defensive tracing
            //else
            //{
            //    //new value is being set by me, skip processing.
            //}
            //#endregion
        }

        /// <summary>
        /// Static handler for listening to changes of ValueProperty.
        /// </summary>
        private static void ValuePropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
        {
            NumberBox obj = (NumberBox)target;
            
            //update target object
            obj.OnValueChanged(e);

            //raise event
            obj.RaiseEventPropertyChanged("Value");

            if (!obj.HasFocus)
            {
                obj.ApplyValueToTextWithFormat();
            }
        }
        #endregion

        #endregion

        protected override void OnGotFocus(RoutedEventArgs e)
        {
            mHasFocus = true;
            ApplyValueToTextWithoutFormat();

            //auto select
            this.SelectionStart = 0;
            this.SelectionLength = this.Text.Length;

            base.OnGotFocus(e);
        }

        private void ApplyValueToTextWithoutFormat()
        {
            if (!string.IsNullOrEmpty(mFormat))
            {
                mSuppressValueChange = true;
                this.Text = string.Format("{0}", this.Value);
                mSuppressValueChange = false;
            }
        }

        #region Apply Value To Text With Format
        private void ApplyValueToTextWithFormat()
        {
            if (!string.IsNullOrEmpty(mFormat))
            {
                mSuppressValueChange = true;
                string format = "{0:" + mFormat + "}";
                string txt = string.Format(format, this.Value);
                this.Text = txt;
                mSuppressValueChange = false;
            }
        }
        #endregion

        protected override void OnLostFocus(RoutedEventArgs e)
        {
            mHasFocus = false;
            ApplyValueToTextWithFormat();
            base.OnLostFocus(e);
        }

        protected override void OnTextInput(TextCompositionEventArgs e)
        {
            if (!char.IsDigit(e.Text[0]) && e.Text != "-" && e.Text != ".")
            {
                e.Handled = true;
                return;
            }
            base.OnTextInput(e);
        }

        protected override void OnTextInputUpdate(TextCompositionEventArgs e)
        {
            decimal newValue;
            string newText = this.Text;
            if (decimal.TryParse(newText, out newValue))
            {
                //set new value            
                mSuppressValueChange = true;
                //Value = newValue;
                SetValue(ValueProperty, newValue);
                mSuppressValueChange = false;

                //raise event to support binding
                RaiseEventPropertyChanged("Value");
            }
            base.OnTextInputUpdate(e);
        }

        #region On Key Down
        protected override void OnKeyDown(KeyEventArgs e)
        {
            if (e.Key == Key.Decimal || e.PlatformKeyCode == 190)
            {
                if (mAcceptDecimals && !this.Text.Contains(".")) e.Handled = false;
                else e.Handled = true;
            }
            else if (e.Key == Key.Subtract || e.PlatformKeyCode == 189)
            {
                if (AcceptNegative && !this.Text.Contains("-") && this.SelectionStart == 0) e.Handled = false;
                else e.Handled = true;
            }
            else e.Handled = false;
            base.OnKeyDown(e);
        }
        #endregion

        #region On Text Input Start
        protected override void OnTextInputStart(TextCompositionEventArgs e)
        //protected override void OnPreviewTextInput(TextCompositionEventArgs e)
        {
            bool acceptable = false;
            decimal newValue;

            if (!char.IsDigit(e.Text[0]) && e.Text != "-" && e.Text != ".")
            {
                e.Handled = true;
                return;
            }

            //project new text
            string currentText = this.Text;
            string newText = string.Empty;
            int start = this.SelectionStart;
            int length = this.Text.Length;
            if (start >= length - 1)
            {
                newText = this.Text + e.Text;
            }
            else
            {
                newText = currentText.Insert(start, e.Text);
            }

            //ensure the new text is a number
            if (decimal.TryParse(newText, out newValue))
            {
                //ensure new value is within range
                acceptable = newValue <= mMaxValue && newValue >= mMinValue;

                //ensure decimals are rejected if required
                if (!mAcceptDecimals && newText.Contains(".")) acceptable = false;
                #region Defensive tracing
                else
                {
                    //okay, move on
                }
                #endregion
            }
            else if (newText == "-" && AcceptNegative)
            {
                #region Handle special case where new text is "-"

                acceptable = true;

                #endregion
            }
            else if (e.Text == "." && AcceptDecimals && !currentText.Contains("."))
            {
                #region Handle special case "xxxxxx." but only one . is acceptable

                acceptable = true;

                #endregion
            }
            #region Defensive tracing
            else
            {
                //could not parse
                acceptable = false;
            }
            #endregion

            //allow or disallow new text
            e.Handled = !acceptable;
            e.Handled = true;

            if (acceptable)
            {
                ////set new value
                //mSuppressValueChange = true;
                //Value = newValue;
                //mSuppressValueChange = false;

                ////raise event to support binding
                //RaiseEventPropertyChanged("Value");
            }

            //base.OnTextInputStart(e);
            //base.OnPreviewTextInput(e);
        }
        #endregion

        #region On Text Changed
        void OnTextChanged(object sender, TextChangedEventArgs e)
        {
            if (!mSuppressValueChange)
            {
                string txt = this.Text;

                decimal newValue;
                if (decimal.TryParse(txt, out newValue))
                {
                    //set new value
                    mSuppressValueChange = true;
                    //Value = newValue;
                    SetValue(ValueProperty, newValue);
                    mSuppressValueChange = false;

                    //raise event to support binding
                    RaiseEventPropertyChanged("Value");
                }
                else if (string.IsNullOrEmpty(txt))
                {
                    //set new value
                    mSuppressValueChange = true;
                    //Value = newValue;
                    SetValue(ValueProperty, 0m);
                    mSuppressValueChange = false;

                    //raise event to support binding
                    RaiseEventPropertyChanged("Value");
                }
                #region Defensive tracing
                else
                {
                    //cannot parse
                }
                #endregion
            }
            #region Defensive tracing
            else
            {
                //change is suppressed
            }
            #endregion
        }
        #endregion

        #region INotifyPropertyChanged Members

        #region Property Changed
        /// <summary>
        /// This event is raised after a property is changed.
        /// </summary>
        [Browsable(true)
        , Category("Action")
        , Description("Occurs after a property is changed.")]
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Invoke event PropertyChanged.
        /// </summary>        
        protected void RaiseEventPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                PropertyChangedEventArgs args = new PropertyChangedEventArgs(propertyName);
                handler(this, args);
            }
        }
        #endregion

        #endregion

        #region Constructors
        /// <summary>
        /// Default constructor.
        /// </summary>
        public NumberBox()
            : base()
        {
            this.TextAlignment = System.Windows.TextAlignment.Right;
            this.TextChanged += new TextChangedEventHandler(OnTextChanged);
        }


        #endregion
    }
}
